#include <windows.h>
#include <map>
#include <chrono>

#if USE_RBL_MINHOOK_RUNTIME
	#include "TSfuncs/MinHookMini.h"
#else
	#include "MinHook.h"
#endif

#include "TSfuncs/TSFuncs.cpp"
#include "TSfuncs/TSHooks.cpp"
#include "TSfuncs/TSVector.cpp"

#include "BLstructs/include.hpp"
#include "TSFuncsAddition.h"

int GLOBAL_RADIUS_SEARCH_TIMER = 0;
int GLOBAL_RADIUS_SEARCH_COUNTER = 0;

struct ghostingList
{
	unsigned int index = 0;
	unsigned int size = 0;
	int* list = new int[0];
};

std::map<int, ghostingList*> ghostingLists;

ghostingList* getGhostingList(SimObject* obj)
{
	if(obj == NULL)
		return NULL;

	auto search = ghostingLists.find(obj->id);
	if(search != ghostingLists.end())
		return search->second;

	return NULL;
}

int clearClient(SimObject* obj)
{
	int total = -1;
	ghostingList* gListOld = getGhostingList(obj);
	if(gListOld != NULL)
	{
		total = gListOld->size;
		delete[] gListOld->list;
		delete gListOld;
		ghostingLists.erase(obj->id);
	}

	return total;
}

int TS_ghostBuddy_newClient(ADDR objADDR, int argc, const char *argv[])
{
	SimObject* obj = (SimObject*)objADDR;

	//Clear if exists
	clearClient(obj);

	SimObject* destSetSO = (SimObject*)tsf_FindObject(argv[2]);
	if(destSetSO == NULL)
		return -1;

	if(NamespaceClasses_instanceOf(destSetSO, "SimSet") <= 0)
		return -2;

	SimSet* destSet = (SimSet*)destSetSO;

	ghostingList* gList = new ghostingList;
	ghostingLists.insert(std::make_pair(obj->id, gList));

	gList->size = destSet->mElementCount;
	int *brickArray = new int[destSet->mElementCount];
	gList->list = brickArray;

	for(unsigned int i = 0; i < destSet->mElementCount; i++)
	{
		SimObject* elem = (*destSet->mArray)[i];
		if(elem != NULL)
			brickArray[i] = elem->id;
	}

	return destSet->mElementCount;
}

int TS_ghostBuddy_ghostNext(ADDR objADDR, int argc, const char *argv[])
{
	SimObject* obj = (SimObject*)objADDR;
	ghostingList* gL = getGhostingList(obj);

	//Never initialized
	if(gL == NULL)
		return -1;

	//num bricks negative?
	int numBricks = atoi(argv[2]);
	if(numBricks <= 0)
		return -2;


	//Fully ghosted
	if(gL->index >= gL->size)
		return 0;

	int i = 0;
	for(; gL->index < gL->size; gL->index++)
	{
		if(i >= numBricks)
			break;

		int ghostObjID = gL->list[gL->index];
		SimObject* ghostObj = (SimObject*)FIXEDtsf_FindObject(ghostObjID);
		if(ghostObj != NULL)
		{
			static char buf[16];
			snprintf(buf, 16, "%u", obj->id);
			tsf_BlCon__executefSimObj((ADDR*)ghostObj, 2, "scopeToClient", buf);
		}

		i++;
	}

	return i;
}

int TS_ghostBuddy_getCount(ADDR objADDR, int argc, const char *argv[])
{
	SimObject* obj = (SimObject*)objADDR;

	if(ghostingLists[obj->id] != NULL)
		return ghostingLists[obj->id]->size;

	return 0;
}

int TS_ghostBuddy_getIndex(ADDR objADDR, int argc, const char *argv[])
{
	SimObject* obj = (SimObject*)objADDR;

	if(ghostingLists[obj->id] != NULL)
		return ghostingLists[obj->id]->index;

	return 0;
}

int TS_ghostBuddy_clearClient(ADDR objADDR, int argc, const char *argv[])
{
	SimObject* obj = (SimObject*)objADDR;
	return clearClient(obj);
}

int TS_ghostBuddy_rescope(ADDR objADDR, int argc, const char *argv[])
{
	GameConnection* obj = (GameConnection*)objADDR;
	return obj->simtimeLastScopeQuery -= 5001;
}

int TS_ghostBuddy_getSimtimeLastScopeQuery(ADDR objADDR, int argc, const char *argv[])
{
	GameConnection* obj = (GameConnection*)objADDR;
	return obj->simtimeLastScopeQuery;
}

void scope_query_metrics_complete(NetObject* obj, ConnectionProtocol_NetConnection* NetConn, int diff)
{
	if(NetConn == NULL)
		return;
	
	//NetConnection is a dual inheritor of ConnectionProtocol and SimObject
	//Advance the pointer (160 bytes) and jump over the ConnectionProtocol section to handle this as a SimObject
	//Reinterpret as unsigned char* to move pointer in multiple of a single byte
	NetObject* NetConn_NetObject = (NetObject*)(((unsigned char*)NetConn) + ClassStruct_NetConnectionConnectionProtocol_to_NetConnectionSimObject_offset);
	
	//Needed because TSfuncs was originally written with non-pointer addresses stored as int
	//The compiler will otherwise automatically dereference
	unsigned int NetConn_NetObject_uint = (unsigned int)NetConn_NetObject;

	char gen_buffer[32];

	//Increase cumulative time on object
	if(diff > 0)
	{
		GLOBAL_RADIUS_SEARCH_TIMER += diff;
		
		const char *acc_time_chr = tsf_GetDataField(NetConn_NetObject_uint, "ghostBuddy_total_radius_search_time", NULL);
		int acc_time_int = 0;
		if(acc_time_chr != NULL && *acc_time_chr != '\0')
			acc_time_int = atoi(acc_time_chr);
			
		acc_time_int += diff;

		itoa(acc_time_int, gen_buffer, 10);
		tsf_SetDataField(NetConn_NetObject_uint, "ghostBuddy_total_radius_search_time", NULL, gen_buffer);
	}
	
	GLOBAL_RADIUS_SEARCH_COUNTER += 1;
	
	//increase hit counter
	const char *hit_count_chr = tsf_GetDataField(NetConn_NetObject_uint, "ghostBuddy_total_radius_search_counter", NULL);
	int hit_count_int = 0;
	if(hit_count_chr != NULL && *hit_count_chr != '\0')
		hit_count_int = atoi(hit_count_chr);
		
	hit_count_int += 1;

	itoa(hit_count_int, gen_buffer, 10);
	tsf_SetDataField(NetConn_NetObject_uint, "ghostBuddy_total_radius_search_counter", NULL, gen_buffer);
}



boolean in_any_camera_scope_query = false;
int enable_brick_scope_blocking = false;


//Cameras (when orbing or dead) do not use Player::onCameraScopeQuery()
BlFunctionDef(void, __thiscall, ShapeBase__onCameraScopeQuery, void*, void*, void*);
BlFunctionHookDef(ShapeBase__onCameraScopeQuery);

void __fastcall ShapeBase__onCameraScopeQueryHook(Player* obj, void* blank, ConnectionProtocol_NetConnection* NetConn, void* CamScopeQ)
{
	//Whether this function was the first onCameraScopeQuery() to run
	bool controlling_metrics = false;

	if(!in_any_camera_scope_query)
	{
		controlling_metrics = true;
		in_any_camera_scope_query = true;
	}
	
	auto now = std::chrono::high_resolution_clock::now();
	
	ShapeBase__onCameraScopeQueryOriginal(obj, NetConn, CamScopeQ);
	
	if(controlling_metrics)
	{
		in_any_camera_scope_query = false;
		auto then = std::chrono::high_resolution_clock::now();
		int diff = std::chrono::duration_cast<std::chrono::milliseconds>(then-now).count();
		scope_query_metrics_complete((NetObject*)obj, NetConn, diff);
	}
}

//TODO: variable to allow one-time scoping?
/*
	if(atoi(argv[2]) || stricmp(argv[2], "true") == 0)
	
*/
//Players use Player::onCameraScopeQuery() and call parent ShapeBase::onCameraScopeQuery()
BlFunctionDef(void, __thiscall, Player__onCameraScopeQuery, void*, void*, void*);
BlFunctionHookDef(Player__onCameraScopeQuery);

void __fastcall Player__onCameraScopeQueryHook(Player* obj, void* blank, ConnectionProtocol_NetConnection* NetConn, void* CamScopeQ)
{
	//Whether this function was the first onCameraScopeQuery() to run
	bool controlling_metrics = false;

	if(!in_any_camera_scope_query)
	{
		controlling_metrics = true;
		in_any_camera_scope_query = true;
	}
	
	auto now = std::chrono::high_resolution_clock::now();
	
	Player__onCameraScopeQueryOriginal(obj, NetConn, CamScopeQ);
	
	if(controlling_metrics)
	{
		in_any_camera_scope_query = false;
		auto then = std::chrono::high_resolution_clock::now();
		int diff = std::chrono::duration_cast<std::chrono::milliseconds>(then-now).count();
		scope_query_metrics_complete((NetObject*)obj, NetConn, diff);
	}
}


BlFunctionDef(void, __thiscall, octTree__findObjects, ADDR, float*, unsigned int, void*, void*);
BlFunctionHookDef(octTree__findObjects);

//this, *, box3f, mask, callback, key
void __fastcall octTree__findObjectsHook(ADDR obj, void* blank, float* arg1, unsigned int mask, void* arg3, void* arg4)
{
	if(!enable_brick_scope_blocking || !in_any_camera_scope_query)
	{
		octTree__findObjectsOriginal(obj, arg1, mask, arg3, arg4);
		return;
	}


	//BrickType is 100663296 is 0000 0110 0000 0000 0*
	//bits 24, 25, 26
	//int TypeMasks__All = 1 << 31;
	unsigned int TypeMasks__FxBrickAlwaysObjectType = 1 << 26;
	unsigned int TypeMasks__FxBrickObjectType = 1 << 25;
	//int TypeMasks__StaticTSObjectType = 1 << 24;

	unsigned int bricks = TypeMasks__FxBrickObjectType | TypeMasks__FxBrickAlwaysObjectType;
	//=0x6000000

	mask = mask & ~bricks;
	octTree__findObjectsOriginal(obj, arg1, mask, arg3, arg4);
}

bool init()
{
	if(!tsh_InitInternal())
		return false;
		
	if (!tsf_InitInternal())
		return false;
		
	if(!tsh_InitMinHook())
		return false;
	
	#if(USE_RBL_MINHOOK_RUNTIME)
		HMODULE rbl_hmodule;
		bool found_rbl_module = GetModuleHandleExA(0, "RedBlocklandLoader.dll", &rbl_hmodule);
		if(found_rbl_module)
		{
			if(rbl_hmodule != NULL)
			{
				FARPROC fpRBL_ABI_version = GetProcAddress(rbl_hmodule, "RBL_ABI_version");
				if(fpRBL_ABI_version == NULL)
				{
					BlPrintf("    - RedBlocklandLoader does not have an ABI version");
					BlPrintf("    - %s was not compiled with MinHook and can't proceed without a compatible RedBlocklandLoader", PROJECT_NAME);
					return false;
				}

				else
				{
					int RBL_ABI_version = *(int*)fpRBL_ABI_version;
					BlPrintf("    - Detected RBL ABI version is found: %d", RBL_ABI_version);

					MH_ApplyQueued = GetProcAddress(rbl_hmodule, "exMH_ApplyQueued@0");
					BlPrintf("    - MH_ApplyQueued = %p", MH_ApplyQueued);

					MH_CreateHook = GetProcAddress(rbl_hmodule, "exMH_CreateHook@12");
					BlPrintf("    - MH_CreateHook = %p", MH_CreateHook);

					MH_CreateHookApi = GetProcAddress(rbl_hmodule, "exMH_CreateHookApi@16");
					BlPrintf("    - MH_CreateHookApi = %p", MH_CreateHookApi);

					MH_CreateHookApiEx = GetProcAddress(rbl_hmodule, "exMH_CreateHookApiEx@20");
					BlPrintf("    - MH_CreateHookApiEx = %p", MH_CreateHookApiEx);

					MH_DisableHook = GetProcAddress(rbl_hmodule, "exMH_DisableHook@4");
					BlPrintf("    - MH_DisableHook = %p", MH_DisableHook);

					MH_EnableHook = GetProcAddress(rbl_hmodule, "exMH_EnableHook@4");
					BlPrintf("    - MH_EnableHook = %p", MH_EnableHook);

					MH_Initialize = GetProcAddress(rbl_hmodule, "exMH_Initialize@0");
					BlPrintf("    - MH_Initialize = %p", MH_Initialize);

					MH_QueueDisableHook = GetProcAddress(rbl_hmodule, "exMH_QueueDisableHook@4");
					BlPrintf("    - MH_QueueDisableHook = %p", MH_QueueDisableHook);

					MH_QueueEnableHook = GetProcAddress(rbl_hmodule, "exMH_QueueEnableHook@4");
					BlPrintf("    - MH_QueueEnableHook = %p", MH_QueueEnableHook);

					MH_RemoveHook = GetProcAddress(rbl_hmodule, "exMH_RemoveHook@4");
					BlPrintf("    - MH_RemoveHook = %p", MH_RemoveHook);

					MH_StatusToString = GetProcAddress(rbl_hmodule, "exMH_StatusToString@4");
					BlPrintf("    - MH_StatusToString = %p", MH_StatusToString);

					MH_Uninitialize = GetProcAddress(rbl_hmodule, "exMH_Uninitialize@0");
					BlPrintf("    - MH_Uninitialize = %p", MH_Uninitialize);
				}
			}
		}
	#endif	

	//Not known to ever be called?
	//0x758530	NetObject::vftable+20
	//0x59a980	NetObject::onCameraScopeQuery()
	//BlScanFunctionHex(NetObject__onCameraScopeQuery, "xxx");

	//Not known to ever be called?
	//0x72c630	ShapeBase::vftable+20
	//0x4f2c60	ShapeBase::onCameraScopeQuery()
	BlScanFunctionHex(ShapeBase__onCameraScopeQuery, "55 8b ec 6a ff 68 ? ? ? ? 64 a1 00 00 00 00 50 81 ec 90 00 00 00");
	BlCreateHook(ShapeBase__onCameraScopeQuery);
	BlTestEnableHook(ShapeBase__onCameraScopeQuery);

	//0x72ac64	Player::vftable+20
	//0x4d6ec0	Player::onCameraScopeQuery()
	BlScanFunctionHex(Player__onCameraScopeQuery, "56 8b f1 83 be 40 06 00 00 00"); 
	BlCreateHook(Player__onCameraScopeQuery);
	BlTestEnableHook(Player__onCameraScopeQuery);

	//0x425440	octTree::findObjects
	//non-vftable object call
	BlScanFunctionHex(octTree__findObjects, "83 ec 2c f7 44 24 34 00 00 00 06");
	BlCreateHook(octTree__findObjects);
	BlTestEnableHook(octTree__findObjects);

	tsf_AddVar("GhostBuddy::brickScopeBlocking", &enable_brick_scope_blocking);
	tsf_AddVar("GhostBuddy::totalRadiusSearchTime", &GLOBAL_RADIUS_SEARCH_TIMER);
	tsf_AddVar("GhostBuddy::totalRadiusSearchCounter", &GLOBAL_RADIUS_SEARCH_COUNTER);

	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_newClient", TS_ghostBuddy_newClient,
		"(group) - setup the client for ghosting and copy list of objects from the given SimSet", 3, 3);
	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_clearClient", TS_ghostBuddy_clearClient, "() - clear data for this client", 2, 2);
	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_ghostNext", TS_ghostBuddy_ghostNext, "(int num) - ghost next num bricks", 3, 3);
	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_getIndex", TS_ghostBuddy_getIndex, "() - get total amount of bricks to be ghosted", 2, 2);
	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_getCount", TS_ghostBuddy_getCount, "() - get number of bricks ghosted so far", 2, 2);


	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_rescope", TS_ghostBuddy_rescope,
		"() - Immediately make the client query for nearby bricks", 2, 2);
	tsf_AddConsoleFunc(NULL, "GameConnection", "ghostBuddy_getSimtimeLastScopeQuery", TS_ghostBuddy_getSimtimeLastScopeQuery,
		"() - get the simtime of when the last brick scoping query occurred", 2, 2);
	
	BlPrintf("%s (v%s-%s): init'd", PROJECT_NAME, PROJECT_VERSION, TSFUNCS_DEBUG ? "debug":"release");

	return true;
}

bool deinit(bool terminating)
{
	printf("%s: deinit'd\n", PROJECT_NAME);
	if(!terminating)
	{
		//TODO: clear out ghostingList structs
		BlTestDisableHook(Player__onCameraScopeQuery);
		BlTestDisableHook(octTree__findObjects);
	}

	return true;
}

bool __stdcall DllMain(HINSTANCE hinstance, int reason, void *reserved)
{
	switch (reason)
	{
		case DLL_PROCESS_ATTACH:
			return init();
		case DLL_PROCESS_DETACH:
			return deinit(reserved != NULL);
		default:
			return true;
	}
}

extern "C" void __declspec(dllexport) __cdecl PROJECT_EXPORT(){}
